package com.zjw.socket.demo.starter.plugin.impl.database;

import com.zjw.socket.demo.starter.annotation.NonNull;
import com.zjw.socket.demo.starter.annotation.core.Plugin;
import com.zjw.socket.demo.starter.config.ApplicationConfig;
import com.zjw.socket.demo.starter.core.Reflector;
import com.zjw.socket.demo.starter.core.ioc.IOCBeanInfo;
import com.zjw.socket.demo.starter.core.ioc.IOCObserver;
import com.zjw.socket.demo.starter.core.ioc.IOCScope;
import com.zjw.socket.demo.starter.plugin.IPlugin;
import com.zjw.socket.demo.starter.smi.IFutureTask;
import com.zjw.socket.demo.starter.utils.LogUtils;
import io.vertx.core.Vertx;
import io.vertx.core.json.JsonObject;
import io.vertx.mysqlclient.MySQLConnectOptions;
import io.vertx.mysqlclient.MySQLPool;
import io.vertx.sqlclient.PoolOptions;
import io.vertx.sqlclient.Row;
import io.vertx.sqlclient.SqlClient;

import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 数据库插件，提供一些数据库基本操作
 *
 * @see SQL
 * @see SQLMapping
 */
@Plugin
public class DatabasePlugin implements IPlugin {
  public static final String ENGINE_MYSQL = "mysql";
  private final String TAG = this.getClass().getSimpleName();
  protected final String host = ApplicationConfig.getString("database.host");
  protected final int port = ApplicationConfig.getInt("database.port");
  protected final String dataName = ApplicationConfig.getString("database.dataname");
  protected final String user = ApplicationConfig.getString("database.user");
  protected final String password = ApplicationConfig.getString("database.password");

  protected final String engine = ApplicationConfig.getString("database.engine");
  private static final String PRE_SQL_PARAMS = ":";
  private final Pattern sqlParamsPattern = Pattern.compile(PRE_SQL_PARAMS + "[a-zA-z]+");

  private final static SqlExecutor sqlExecutor = new SqlExecutor();
  @NonNull
  protected SqlClient client;

  @Override
  public void onPlugin(Vertx vertx) {
    if (ENGINE_MYSQL.equals(engine)) {
      MySQLConnectOptions connectOptions = new MySQLConnectOptions().setPort(port).setHost(host).setDatabase(dataName).setUser(user).setPassword(password);

      PoolOptions poolOptions = new PoolOptions().setMaxSize(5);
      client = MySQLPool.client(connectOptions, poolOptions);
    }

    dispatchMappingScan(vertx);
  }

  private void dispatchMappingScan(Vertx vertx) {
    IOCScope.globalScope.registerRefreshObserver(new IOCObserver() {
      @Override
      public void onRefresh(Class<?> clazz) {
        if (!clazz.isAnnotationPresent(SQLMapping.class)) {
          return;
        }
        LogUtils.debug(TAG, "start parse mapping class " + clazz.getName());
        Object mapping = parseMapping(clazz);
        IOCScope.globalScope.putBean(mapping.getClass().getInterfaces()[0], mapping);
        LogUtils.debug(TAG, "end parse mapping class " + clazz.getName());
      }

      @Override
      public void onInject(IOCBeanInfo o) {

      }

      @Override
      public void onComplete() {

      }
    });
  }

  /**
   * 解析对应Mapping类，如果无法解析，抛出异常
   *
   * @throws RuntimeException 解析错误
   */
  @NonNull
  private Object parseMapping(Class<?> clazz) {
    SQLMapping annotation = clazz.getAnnotation(SQLMapping.class);
    if (annotation == null) {
      throw new RuntimeException("SQLMapping is not present " + clazz);
    }

    if (!clazz.isInterface()) {
      throw new RuntimeException("SQLMapping is not interface " + clazz);
    }

    return parseMappingImpl(clazz);
  }

  private Object parseMappingImpl(Class<?> clazz) {
    Map<Method, SQLMethodInfo> sqlMethodMap = new HashMap<>();


    Method[] declaredMethods = clazz.getDeclaredMethods();

    for (Method declaredMethod : declaredMethods) {
      SQL sql = declaredMethod.getAnnotation(SQL.class);
      if (sql == null) {
        continue;
      }

      SQLMethodInfo sqlMethodInfo = createSQLMethodInfo(declaredMethod);

      sqlMethodMap.put(declaredMethod, sqlMethodInfo);
    }

    return Proxy.newProxyInstance(clazz.getClassLoader(), new Class[]{clazz}, new DatabaseSQLProxy(sqlMethodMap));
  }

  private SQLMethodInfo createSQLMethodInfo(Method declaredMethod) {

    SQL sql = declaredMethod.getAnnotation(SQL.class);
    SQLMethodInfo methodInfo = new SQLMethodInfo();
    methodInfo.sql = sql.value();
    methodInfo.client = client;
    Parameter[] parameters = declaredMethod.getParameters();

    if (parameters.length < 1 || !Reflector.isSuperClassOrInterfaceImpl(parameters[0].getType(), IFutureTask.class)) {
      throw new IllegalArgumentException("params 0 must be future call");
    }

    Matcher matcher = sqlParamsPattern.matcher(sql.value());
    List<String> params = new ArrayList<>();
    while (matcher.find()) {
      String param = matcher.group();
      params.add(param);
    }

    //call+params = 1 + params
    if (params.size() != parameters.length - 1) {
      throw new IllegalArgumentException("params.size() != parameters.count!");
    }

    String[] paramsNameArray = new String[parameters.length - 1];
    for (int i = 1; i < parameters.length; i++) {
      Parameter parameter = parameters[i];
      SQLParams sqlParamsName = parameter.getAnnotation(SQLParams.class);
      if (sqlParamsName == null) {
        throw new IllegalArgumentException("no present SQLParamsName for " + parameter);
      }
      String value = sqlParamsName.value();
      if (!params.contains(PRE_SQL_PARAMS + value)) {
        //不存在对应字段
        throw new IllegalArgumentException("params not present for " + value);
      }
      paramsNameArray[i - 1] = value;
    }

    methodInfo.fieldArray = parameters;
    methodInfo.fieldNameArray = paramsNameArray;

    methodInfo.callType = Reflector.findRegexClassByContent(parameters[0], List.class);

    Class<? extends IRowConvert> convert = sql.convert();

    if (convert.equals(IRowConvert.class)) {
      if (methodInfo.callType == null) {
        throw new IllegalArgumentException("not find class for " + parameters[0]);
      }
      //支持自动生成convert
      methodInfo.convert = createConvertByResult(methodInfo.callType);
    } else {
      Object o = Reflector.instanceEmptyConstructor(convert);
      if (o == null) {
        throw new IllegalArgumentException("is not present empty constructor for " + convert.getName());
      }
      methodInfo.convert = (IRowConvert<?>) o;
    }

    return methodInfo;
  }

  private IRowConvert<?> createConvertByResult(Class<?> resultType) {
    Field[] declaredFields = resultType.getDeclaredFields();
    ObjectRowConvert convert = new ObjectRowConvert();

    Map<String, Field> fieldHashMap = new HashMap<>();
    for (Field declaredField : declaredFields) {
      SQLField sqlField = declaredField.getAnnotation(SQLField.class);
      if (sqlField == null) {
        continue;
      }

      String sqlName = sqlField.value();
      fieldHashMap.put(sqlName, declaredField);
    }

    convert.target = resultType;
    convert.map = fieldHashMap;
    return convert;
  }

  /**
   * 通用Convert
   */
  private static class ObjectRowConvert implements IRowConvert<Object> {

    Map<String, Field> map;

    Class<?> target;

    @Override
    public Object convert(Row row) {
      JsonObject entries = row.toJson();

      Object o = Reflector.instanceEmptyConstructor(target);

      for (Map.Entry<String, Field> entry : map.entrySet()) {
        String key = entry.getKey();
        Field field = entry.getValue();
        Class<?> type = field.getType();
        if (type.equals(Integer.class)) {
          Integer value = entries.getInteger(key);
          Reflector.safeSetFiled(o, field, value);
        } else if (type.equals(Boolean.class)) {
          Boolean integer = entries.getBoolean(key);
          Reflector.safeSetFiled(o, field, integer);

        } else if (type.equals(String.class)) {
          String value = entries.getString(key);
          Reflector.safeSetFiled(o, field, value);

        } else if (type.equals(Float.class)) {
          Float value = entries.getFloat(key);
          Reflector.safeSetFiled(o, field, value);

        } else if (type.equals(Double.class)) {
          Double value = entries.getDouble(key);
          Reflector.safeSetFiled(o, field, value);
        } else if (type.equals(byte[].class)) {
          byte[] value = entries.getBinary(key);
          Reflector.safeSetFiled(o, field, value);
        } else {
          throw new IllegalArgumentException("unknow field type for " + entry);
        }
      }
      return o;
    }
  }

  private static class SQLMethodInfo {
    //参数映射
    Parameter[] fieldArray;
    String[] fieldNameArray;
    IRowConvert<?> convert;
    Class<?> callType;
    String sql;
    SqlClient client;
    IFutureTask<List<?>> call;

    SQLMethodInfo() {

    }

    /*
     * 根据args解析sql
     * */
    private String parseSql(Object[] args) {
      String realSql = sql;
      for (int i = 1; i < fieldArray.length; i++) {
        String realParam = args[i].toString();
        checkSafeSql(realParam);
        String stub = PRE_SQL_PARAMS + fieldNameArray[i - 1];
        realSql = realSql.replace(stub, "\"" + realParam + "\"");
      }
      return realSql;
    }

    /*
     * 检查sql注入问题
     */
    private void checkSafeSql(String realParam) {
      String s = realParam.toLowerCase();
      if (s.contains("select") || s.contains("update") || s.contains("delete") || s.contains("insert") || s.contains("=")) {
        throw new RuntimeException("bad sql for " + realParam);
      }
    }


    /*
     * 执行器
     * */
    @SuppressWarnings("unchecked")
    void invoke(Object[] args) {
      call = (IFutureTask<List<?>>) args[0];
      String realSql = parseSql(args);

      sqlExecutor.doRealSqlExecute(client, convert, call, realSql);
    }
  }

  private static class DatabaseSQLProxy implements InvocationHandler {

    @NonNull
    private final Map<Method, SQLMethodInfo> sqlMethodMap;

    public DatabaseSQLProxy(Map<Method, SQLMethodInfo> sqlMethodMap) {
      this.sqlMethodMap = sqlMethodMap;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      SQLMethodInfo sqlMethodInfo = sqlMethodMap.get(method);
      if (sqlMethodInfo == null) {
        throw new RuntimeException("can't find SQLMethodInfo for method " + method);
      }

      sqlMethodInfo.invoke(args);
      return null;
    }
  }

}
